/**
 * Project:   warnme-app
 * File:      FollowMeLocationSource.java
 * License: 
 *            This file is licensed under GNU General Public License version 3
 *            http://www.gnu.org/licenses/gpl-3.0.txt
 *
 * Copyright: Bartosz Cichecki [ cichecki.bartosz@gmail.com ]
 * Date:      Mar 23, 2014
 */

package dtu.ds.warnme.app.location;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.http.Header;
import org.apache.http.HttpStatus;

import android.app.Activity;
import android.content.Context;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.util.Log;

import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.LocationSource;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.gson.reflect.TypeToken;

import dtu.ds.warnme.app.application.Prefs;
import dtu.ds.warnme.app.holders.UserProfileHolder;
import dtu.ds.warnme.app.model.impl.Event;
import dtu.ds.warnme.app.model.impl.EventType;
import dtu.ds.warnme.app.ws.client.https.GsonHttpResponseHandler;
import dtu.ds.warnme.app.ws.client.restful.RestClientHolder;

/**
 * @author Bartosz Cichecki
 * 
 */
public class FollowMeLocationSource implements LocationSource, LocationListener {

	private static final String TAG = "FollowMeLocationSource";

	private OnLocationChangedListener locationChangedListener;

	private LocationManager locationManager;

	private Criteria criteria;

	private String bestAvailableProvider;

	private final int minTime = 2 * 1000;

	private final int minDistance = 5;

	private Context context;

	private GoogleMap map;

	private Location lastPullLocation;

	private Location lastKnownLocation;

	private FollowMeLocationSourceListener locationSourceListener;

	private List<Event> cachedEvents = Collections.synchronizedList(new ArrayList<Event>());

	private Location previousLocation;

	public FollowMeLocationSource(Activity activity, Context context, GoogleMap map) {
		this.context = context;
		this.map = map;

		try {
			locationSourceListener = (FollowMeLocationSourceListener) activity;
		} catch (ClassCastException ex) {
			throw new ClassCastException(activity.toString() + " must implement FollowMeLocationSource");
		}

		init();
	}

	@Override
	public void activate(OnLocationChangedListener listener) {
		Log.i(TAG, "Location source activated.");
		locationChangedListener = listener;

		if (bestAvailableProvider != null) {
			locationManager.requestLocationUpdates(bestAvailableProvider, minTime, minDistance, this);
		} else {
			Log.i(TAG, "No location providers available");
		}
	}

	private void checkEventsProximity(Location location) {
		if (cachedEvents == null || cachedEvents.isEmpty()) {
			return;
		}

		Float closestDistance = Float.MAX_VALUE;
		Event closestEvent = null;

		for (Event event : cachedEvents) {
			Float distance = location.distanceTo(event.getLocation());
			Float bearing = location.bearingTo(event.getLocation());

			if (distance < closestDistance && bearing < 90f) {
				closestDistance = distance;
				closestEvent = event;
			}
		}

		if (closestEvent != null && closestDistance < 500f) {
			locationSourceListener.onApproachingEvent(closestEvent);
		}
	}

	@Override
	public void deactivate() {
		Log.i(TAG, "Location source deactivated.");
		locationManager.removeUpdates(this);
		locationChangedListener = null;
	}

	private boolean eventsNeedRefresh(Location location1, Location location2) {
		return location1.distanceTo(location2) > 0.5 * Prefs.getRadius() || Math.abs(location1.getBearing() - location2.getBearing()) > 30;
	}

	public void getBestAvailableProvider() {
		bestAvailableProvider = locationManager.getBestProvider(criteria, true);
	}

	public Location getCurrentLocation() {
		return lastKnownLocation;
	}

	private void init() {
		locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);

		criteria = new Criteria();
		criteria.setAccuracy(Criteria.ACCURACY_FINE);
		criteria.setPowerRequirement(Criteria.POWER_MEDIUM);
		criteria.setAltitudeRequired(true);
		criteria.setBearingRequired(true);
		criteria.setSpeedRequired(true);
		criteria.setCostAllowed(true);
	}

	@Override
	public void onLocationChanged(Location location) {
		Log.i(TAG, "Location changed: " + location);

		if (!location.hasBearing()) {
			Log.w(TAG, "Location does not have bearing. Will calculate on our own...");

			if (previousLocation != null) {
				float bearing = previousLocation.bearingTo(location);
				location.setBearing(bearing);

				Log.w(TAG, "New bearing: " + bearing);
			}

			previousLocation = location;
		}

		if (locationChangedListener != null) {
			locationChangedListener.onLocationChanged(location);
		}

		CameraPosition cameraPosition = new CameraPosition.Builder().target(new LatLng(location.getLatitude(), location.getLongitude()))
		        .zoom(17).bearing(location.getBearing()).tilt(40).build();
		map.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));

		lastKnownLocation = location;

		pullNewEvents(location);
		checkEventsProximity(location);
	}

	@Override
	public void onProviderDisabled(String provider) {
		Log.i(TAG, "Location provider disabled: " + provider);
		RestClientHolder.getRestClient().cancelRequests(context, true);
	}

	@Override
	public void onProviderEnabled(String provider) {
		Log.i(TAG, "Location provider enabled: " + provider);
	}

	@Override
	public void onStatusChanged(String provider, int status, Bundle extras) {
		Log.i(TAG, "Location provider status changed: " + provider);
	}

	private void pullNewEvents(final Location newLocation) {
		if (!UserProfileHolder.isLoggedIn()) {
			Log.i(TAG, "User is not logged in. Will not pull events data.");
			return;
		}

		if (lastPullLocation == null || eventsNeedRefresh(newLocation, lastPullLocation)) {
			if (lastPullLocation == null) {
				lastPullLocation = newLocation;
			}

			RestClientHolder.getRestClient().getEvents(context, newLocation, Prefs.getRadius(), EventType.values(),
			        new GsonHttpResponseHandler<List<Event>>(new TypeToken<List<Event>>() {
			        }.getType()) {

				        @Override
				        public void onFailure(int statusCode, Header[] headers, String responseBody, Throwable error) {
					        // If the request failed then too bad...
					        Log.e(TAG, "Couldn't fetch events list. [statusCode=" + statusCode + ", error=" + error);

					        if (statusCode == 0) {
						        locationSourceListener.onDownloadEventsFailed();
					        }

					        if (statusCode == HttpStatus.SC_UNAUTHORIZED) {
						        locationSourceListener.onCredentialsExpired();
					        }
				        }

				        @Override
				        public void onSuccess(int statusCode, Header[] headers, List<Event> events) {
					        locationSourceListener.onDownloadEventsSuccessful();

					        cachedEvents.clear();
					        cachedEvents.addAll(events);

					        map.clear();

					        for (Event event : events) {
						        MarkerOptions markerOptions = new MarkerOptions();
						        markerOptions.position(event.getPostition());
						        map.addMarker(markerOptions);
					        }

					        lastPullLocation = newLocation;
				        }
			        });
		}
	}

}
